package io.gitee.zhangbinhub.acp.cloud.oauth.conf;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.gitee.zhangbinhub.acp.boot.exceptions.ServletExceptionHandler;
import io.gitee.zhangbinhub.acp.boot.exceptions.WebException;
import io.gitee.zhangbinhub.acp.cloud.oauth.authentication.OAuth2UserPasswordAuthenticationConverter;
import io.gitee.zhangbinhub.acp.cloud.oauth.authentication.OAuth2UserPasswordAuthenticationProvider;
import io.gitee.zhangbinhub.acp.cloud.oauth.domain.SecurityUserDetailsService;
import io.gitee.zhangbinhub.acp.cloud.resource.server.AcpCloudResourceServerComponentAutoConfiguration;
import io.gitee.zhangbinhub.acp.cloud.resource.server.component.AcpOpaqueTokenIntrospect;
import io.gitee.zhangbinhub.acp.cloud.resource.server.conf.AcpCloudResourceServerConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;

/**
 * @author zhangbin by 11/04/2018 14:34
 * @since JDK 11
 */
@Configuration
@EnableMethodSecurity
public class AuthorizationServerConfiguration {
    private final ObjectMapper objectMapper;
    private final ServletExceptionHandler servletExceptionHandler;
    private final AcpCloudResourceServerConfiguration acpCloudResourceServerConfiguration;
    private final AcpCloudResourceServerComponentAutoConfiguration acpCloudResourceServerComponentAutoConfiguration;
    private final SecurityUserDetailsService securityUserDetailsService;
    private final OAuth2ResourceServerProperties oAuth2ResourceServerProperties;
    private final RestTemplate restTemplate;

    @Autowired
    public AuthorizationServerConfiguration(ObjectMapper objectMapper,
                                            ServletExceptionHandler servletExceptionHandler, AcpCloudResourceServerConfiguration acpCloudResourceServerConfiguration,
                                            AcpCloudResourceServerComponentAutoConfiguration acpCloudResourceServerComponentAutoConfiguration,
                                            SecurityUserDetailsService securityUserDetailsService,
                                            OAuth2ResourceServerProperties oAuth2ResourceServerProperties,
                                            @Qualifier("acpSpringCloudResourceServerRestTemplate")
                                            RestTemplate restTemplate) {
        this.objectMapper = objectMapper;
        this.servletExceptionHandler = servletExceptionHandler;
        this.acpCloudResourceServerConfiguration = acpCloudResourceServerConfiguration;
        this.acpCloudResourceServerComponentAutoConfiguration = acpCloudResourceServerComponentAutoConfiguration;
        this.securityUserDetailsService = securityUserDetailsService;
        this.oAuth2ResourceServerProperties = oAuth2ResourceServerProperties;
        this.restTemplate = restTemplate;
    }

    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
        // 配置 endpoint 策略
        List<String> permitAll = acpCloudResourceServerComponentAutoConfiguration.webPermitAllPath();
        permitAll.add("/oauth/token");
        httpSecurity.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests.requestMatchers(endpointsMatcher).permitAll()
                                .requestMatchers(permitAll.stream().map(AntPathRequestMatcher::new).toArray(AntPathRequestMatcher[]::new)).permitAll()
                                .anyRequest().authenticated()
                ).userDetailsService(securityUserDetailsService)
                .httpBasic(Customizer.withDefaults())
                .apply(authorizationServerConfigurer);
        // 自定义token端点配置
        OAuth2TokenGenerator<OAuth2Token> tokenGenerator = new DelegatingOAuth2TokenGenerator(new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
        authorizationServerConfigurer.tokenEndpoint(tokenEndpoint -> {
            tokenEndpoint.accessTokenRequestConverter(new DelegatingAuthenticationConverter(Arrays.asList(
                    new OAuth2AuthorizationCodeAuthenticationConverter(),
                    new OAuth2RefreshTokenAuthenticationConverter(),
                    new OAuth2ClientCredentialsAuthenticationConverter(),
                    new OAuth2UserPasswordAuthenticationConverter())));
            OAuth2AuthorizationService authorizationService = authorizationService();
            tokenEndpoint.authenticationProvider(new OAuth2UserPasswordAuthenticationProvider(objectMapper, securityUserDetailsService, tokenGenerator, authorizationService))
                    .authenticationProvider(new OAuth2RefreshTokenAuthenticationProvider(authorizationService, tokenGenerator));
            tokenEndpoint.errorResponseHandler((request, response, exception) -> servletExceptionHandler.responseGlobalException(response, new WebException(HttpStatus.UNAUTHORIZED, exception.getMessage())));
        }).tokenGenerator(tokenGenerator);
        // 关闭session
        httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
        // token 校验
        httpSecurity.oauth2ResourceServer(configurer -> {
            configurer.opaqueToken(customize -> customize.introspector(opaqueTokenIntrospect()));
            configurer.authenticationEntryPoint((request, response, authException) -> servletExceptionHandler.responseGlobalException(response, authException));
            configurer.accessDeniedHandler((request, response, accessDeniedException) -> servletExceptionHandler.responseGlobalException(response, accessDeniedException));
        });
        return httpSecurity.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClientInner = RegisteredClient.withId("acpCloudInnerClient")
                .clientId(acpCloudResourceServerConfiguration.getClientId())
                .clientSecret(acpCloudResourceServerConfiguration.getClientSecret())
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .scopes(scopes -> scopes.addAll(Arrays.asList("INNER".split(","))))
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                        .reuseRefreshTokens(false)
                        .accessTokenTimeToLive(Duration.ofSeconds(600))
                        .refreshTokenTimeToLive(Duration.ofSeconds(86400)).build())
                .build();
        RegisteredClient registeredClient = RegisteredClient.withId("123456")
                .clientId("test")
                .clientSecret("test_sc")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(new AuthorizationGrantType("password"))
                .scopes(scopes -> scopes.addAll(Arrays.asList("ALL,TEST".split(","))))
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                        .reuseRefreshTokens(true)
                        .accessTokenTimeToLive(Duration.ofSeconds(600))
                        .refreshTokenTimeToLive(Duration.ofSeconds(86400)).build())
                .build();
        return new InMemoryRegisteredClientRepository(registeredClientInner, registeredClient);
    }

    /**
     * 自定义OAuth2AuthorizationService
     *
     * @return OAuth2AuthorizationService
     */
    @Bean
    public OAuth2AuthorizationService authorizationService() {
        return new InMemoryOAuth2AuthorizationService();
    }

    @Bean
    public AcpOpaqueTokenIntrospect opaqueTokenIntrospect() {
        return new AcpOpaqueTokenIntrospect(
                oAuth2ResourceServerProperties.getOpaquetoken().getIntrospectionUri(),
                restTemplate
        );
    }

    /**
     * 设置endpoint的url
     *
     * @return ProviderSettings
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .authorizationEndpoint("/oauth/authorize")
                .tokenEndpoint("/inner/oauth/token") // 默认/oauth/token，这里需要自定义重写/oauth/token，绕开Http Basic认证
                .jwkSetEndpoint("/oauth/jwks")
                .tokenRevocationEndpoint("/oauth/revoke")
                .tokenIntrospectionEndpoint("/inner/oauth/introspect")
                .build();
    }
}
